import constants from '@/constants';
import { BaseService } from '@/core/base.service';
import { UopService } from '@/core/uop.service';
import { MarketListDto, MarketOrderDto, MarketSalesDto } from '@/dtos';
import {
  OrderStatusEnum,
  OrderTypeEnum,
  PaymentTypeEnum,
  TokenStatusEnum,
} from '@/enums';
import { halt } from '@/utils';
import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { Asset, Order } from '@prisma/client';
import { Queue } from 'bull';
import dayjs from 'dayjs';
import { SettingService } from './setting.service';
import Decimal from 'decimal.js';
import { YopService } from '@/core/yop.service';
import { SandService } from '@/core/sand.service';
import { UtilService } from "@/core/util.service";

@Injectable()
export class MarketService extends BaseService {
  constructor(
    @InjectQueue('order')
    private readonly orderQueue: Queue,
    @InjectQueue('market')
    private readonly checkQueue: Queue,
    private readonly utilService: UtilService,
    private readonly settingService: SettingService,
    private readonly uopService: UopService,
    private readonly yopService: YopService,
    private readonly sandService: SandService,
  ) {
    super();
  }

  async getList(
    userId,
    { type, albumId, minPrice, maxPrice, title, params, sort }: MarketListDto,
  ) {
    const { current = 1, pageSize = 20, keyword, ...where } = params;

    let sql = `
      SELECT #{fields} FROM Token AS t
      LEFT JOIN Asset AS a ON a.id = t.assetId
      LEFT JOIN Album AS aa ON aa.id = a.albumId  
      ${
        albumId === -1
          ? `
          JOIN UserFollowAsset AS ufa 
          ON ufa.assetId = t.assetId 
            AND ufa.userId = ${userId}
          `
          : ''
      }    
      WHERE t.isDeleted = false
        AND a.isTradable = true       
        ${!!type && type > 0 ? `AND a.albumId = ${type}` : ''}
        ${!!albumId && albumId === -1 ? `AND ufa.assetId = t.assetId` : ''}
        ${!!albumId && albumId > 0 ? `AND aa.id = ${albumId}` : ''}
        ${!!title ? `AND a.title LIKE '%${title}%'` : ''}
        ${!!minPrice && Number(minPrice) > 0 ? `AND t.price >= ${Number(minPrice)}` : ''}
        ${!!maxPrice && Number(maxPrice) > 0 ? `AND t.price <= ${Number(maxPrice)}` : ''}
    `;

    const [{ count }] = await this.prisma.$queryRawUnsafe<[{ count: number }]>(
      sql.replace('#{fields}', 'COUNT(DISTINCT a.id) AS count'),
    );

    const list = await this.prisma.$queryRawUnsafe<Asset[]>(
      `${sql.replace(
        '#{fields}',
        `
          a.id, a.title, a.image, a.limit, a.price, a.circulation, 
          a.highestPrice, a.lowestPrice, 
          (
            SELECT COUNT(tt.id)
            FROM Token AS tt
            WHERE tt.assetId = a.id
              AND (
                tt.status = ${TokenStatusEnum.ON_SALE}
                OR
                tt.status = ${TokenStatusEnum.LOCKING}
              ) 
          ) AS num, 
          (
            SELECT MIN(tt.price)
            FROM Token AS tt
            WHERE tt.assetId = a.id
              AND (
                tt.status = ${TokenStatusEnum.ON_SALE}
                OR
                tt.status = ${TokenStatusEnum.LOCKING}
              ) 
          ) AS price,
          (
            SELECT COUNT(1)
            FROM UserFollowAsset AS ufa 
            WHERE ufa.assetId = t.assetId 
              AND ufa.userId = ${userId}
          ) AS isFollow
        `,
      )}     
      GROUP BY a.id
      ORDER BY ${
        !!sort && Object.keys(sort).length > 0
          ? Object.entries(sort)
              .map(([k, v]) => {
                if (['createdAt', 'circulation'].includes(k)) {
                  return `a.${k} ${v === 'descend' ? 'DESC' : 'ASC'}`;
                }
                if (k === 'price') {
                  return `${k} ${v === 'descend' ? 'DESC' : 'ASC'}`;
                }
                return `t.${k} ${v === 'descend' ? 'DESC' : 'ASC'}`;
              })
              .join(',')
          : 'a.createdAt DESC'
      }
      LIMIT ${pageSize} 
      OFFSET ${(current - 1) * pageSize}
      `,
    );

    const total = count ? Number(count) : 0;
    const totalPage = Math.ceil(total / pageSize);
    const hasMore = current < totalPage;

    return {
      total,
      totalPage,
      hasMore,
      current,
      pageSize,
      list: list.map((o: any) => ({
        ...o,
        num: Number(o.num),
        price: Number(o.price),
        highestPrice: Number(o.highestPrice),
        lowestPrice: Number(o.lowestPrice),
        isFollow: Number(o.isFollow) > 0,
      })),
    };
  }

  async getRecord(assetId) {
    // return {};
    const sql = `
      SELECT #{fields} FROM TokenLog AS log
      LEFT JOIN Token AS t ON t.id = log.tokenId
      LEFT JOIN Asset AS a ON a.id = t.assetId
      WHERE log.isDeleted = false
      AND log.type IN (4,6,7,8)
      AND t.assetId = ${assetId}
      AND log.log LIKE '%买入%'
      AND CONVERT_TZ(log.createdAt, 'UTC', 'Asia/Shanghai') >= '2024-10-21 00:00:00'
    `;

    // const [{ count }] = await this.prisma.$queryRawUnsafe<[{ count: number }]>(
    //   sql.replace('#{fields}', 'COUNT(t.id) AS count'),
    // );

    const list = await this.prisma.$queryRawUnsafe<Order[]>(
      `${sql.replace(
        '#{fields}',
        `
          log.createdAt as payTime, t.no, t.assetId, a.title, REGEXP_REPLACE(log.log, '[^0-9]', '') as unit, a.limit as circulation
        `,
      )}
      ORDER BY log.createdAt DESC
      LIMIT 20
      `,
    );

    const currentDate = dayjs();
    const analysis = await this.prisma.$queryRawUnsafe(`
        SELECT
            (
              SELECT REGEXP_REPLACE(log.log, '[^0-9]', '') FROM TokenLog AS log
              LEFT JOIN Token AS t ON t.id = log.tokenId
              LEFT JOIN Asset AS a ON a.id = t.assetId
              WHERE log.isDeleted = false
              AND log.type IN (4,6,7,8)
              AND t.assetId = ${assetId}
              ORDER BY log.createdAt DESC
              LIMIT 1
            ) AS last,
            (
              SELECT MAX(REGEXP_REPLACE(log.log, '[^0-9]', '')) FROM TokenLog AS log
              LEFT JOIN Token AS t ON t.id = log.tokenId
              LEFT JOIN Asset AS a ON a.id = t.assetId
              WHERE log.isDeleted = false
              AND log.type IN (4,6,7,8)
              AND t.assetId = ${assetId}
              AND 
                  CONVERT_TZ(log.createdAt, 'UTC', 'Asia/Shanghai') >= '${currentDate.startOf('day').format('YYYY-MM-DD HH:mm:ss')}'
              AND 
                  CONVERT_TZ(log.createdAt, 'UTC', 'Asia/Shanghai') <= '${currentDate.endOf('day').format('YYYY-MM-DD HH:mm:ss')}'
            ) AS max,
            (
              SELECT MIN(REGEXP_REPLACE(log.log, '[^0-9]', '')) FROM TokenLog AS log
              LEFT JOIN Token AS t ON t.id = log.tokenId
              LEFT JOIN Asset AS a ON a.id = t.assetId
              WHERE log.isDeleted = false
              AND log.type IN (4,6,7,8)
              AND t.assetId = ${assetId}
              AND 
                  CONVERT_TZ(log.createdAt, 'UTC', 'Asia/Shanghai') >= '${currentDate.startOf('day').format('YYYY-MM-DD HH:mm:ss')}'
              AND 
                  CONVERT_TZ(log.createdAt, 'UTC', 'Asia/Shanghai') <= '${currentDate.endOf('day').format('YYYY-MM-DD HH:mm:ss')}'
            ) AS min,
            (
                SELECT MIN(price) 
                FROM \`Token\`
                WHERE isDeleted = false
                    AND status = 2
                    AND assetId = ${assetId}
            ) AS low
     `);

    // const total = count ? Number(count) : 0;
    // const totalPage = Math.ceil(total / pageSize);
    // const hasMore = current < totalPage;

    return {
      // total,
      // totalPage,
      // hasMore,
      // current,
      // pageSize,
      list: list.map((o) => ({
        ...o,
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-expect-error
        unit: Number(o.unit),
      })),
      last: !!analysis ? Number(analysis[0].last) : {},
      min: !!analysis ? Number(analysis[0].min) : {},
      max: !!analysis ? Number(analysis[0].max) : {},
      low: !!analysis ? Number(analysis[0].low) : {},
    };
  }

  async getSales({ id, params, sort }: MarketSalesDto) {
    const { current = 1, pageSize = 20, keyword, ...where } = params;

    let sql = `
      SELECT #{fields} FROM Token AS t
      LEFT JOIN Asset AS a ON a.id = t.assetId
      LEFT JOIN Album AS aa ON aa.id = a.albumId      
      WHERE t.isDeleted = false
        AND t.assetId = ${id}
        AND t.ownerId != ${constants.default.ownerId}
        AND a.isTradable = true
        AND (
            t.status = ${TokenStatusEnum.ON_SALE}
            OR
            t.status = ${TokenStatusEnum.LOCKING}
        )
    `;

    const [{ count }] = await this.prisma.$queryRawUnsafe<[{ count: number }]>(
      sql.replace('#{fields}', 'COUNT(t.id) AS count'),
    );

    const list = await this.prisma.$queryRawUnsafe<Asset[]>(
      `${sql.replace(
        '#{fields}',
        `
          t.id, t.hash, t.no, t.price, t.status, a.title, a.limit, a.limit as circulation
        `,
      )}
      ORDER BY ${
        !!sort && Object.keys(sort).length > 0
          ? Object.entries(sort)
              .map(([k, v]) => `t.${k} ${v === 'descend' ? 'DESC' : 'ASC'}`)
              .join(',')
          : 't.sellAt DESC'
      }
      LIMIT ${pageSize} 
      OFFSET ${(current - 1) * pageSize}
      `,
    );

    const total = count ? Number(count) : 0;
    const totalPage = Math.ceil(total / pageSize);
    const hasMore = current < totalPage;

    return {
      total,
      totalPage,
      hasMore,
      current,
      pageSize,
      list: list.map((o: any) => ({
        ...o,
        price: Number(o.price),
      })),
    };
  }

  async adminSales({ id, params, sort }: MarketSalesDto) {
    const { current = 1, pageSize = 20, keyword, ...where } = params;

    let sql = `
      SELECT #{fields} FROM Token AS t
      LEFT JOIN Asset AS a ON a.id = t.assetId
      LEFT JOIN Album AS aa ON aa.id = a.albumId
      LEFT JOIN User AS u ON u.id = t.ownerId     
      WHERE t.isDeleted = false
        AND t.assetId = ${id}
        AND t.ownerId != ${constants.default.ownerId}
        AND a.isTradable = true
        AND (
            t.status = ${TokenStatusEnum.ON_SALE}
            OR
            t.status = ${TokenStatusEnum.LOCKING}
        )
    `;

    const [{ count }] = await this.prisma.$queryRawUnsafe<[{ count: number }]>(
      sql.replace('#{fields}', 'COUNT(t.id) AS count'),
    );

    const list = await this.prisma.$queryRawUnsafe<Asset[]>(
      `${sql.replace(
        '#{fields}',
        `
          t.id, t.no, t.price, t.bidPrice, t.bidAt, t.sellAt, t.source, t.status, 
          t.ownerId, a.title, a.limit, u.realname, u.mobile
        `,
      )}
      ORDER BY t.price ASC
      LIMIT ${pageSize} 
      OFFSET ${(current - 1) * pageSize}
      `,
    );

    const total = count ? Number(count) : 0;
    const totalPage = Math.ceil(total / pageSize);
    const hasMore = current < totalPage;

    return {
      total,
      totalPage,
      hasMore,
      current,
      pageSize,
      list: list.map((o: any) => ({
        ...o,
        price: Number(o.price),
        bidPrice: Number(o.bidPrice),
      })),
    };
  }

  async order(
    { id, tokenId, price, num }: MarketOrderDto,
    userId: number,
    ip: string,
  ) {
    return await this.redlock.using([`user:${userId}`], 10000, {}, async () => {
      const user = await this.prisma.user.findUnique({
        where: {
          id: userId,
          isBlockd: false,
          isDeleted: false,
        },
      });
      if (!user) {
        return halt('用户信息异常');
      }
      if (!user.isReal) {
        return halt('请先完成实名认证');
      }
      if (!!num && Number(num) > 1) {
        if (!user?.isBatch) {
          return halt('无批量下单权限');
        }
      }
      const count = await this.prisma.order.count({
        where: {
          ownerId: userId,
          status: OrderStatusEnum.PENDING,
          isDeleted: false,
        },
      });
      if (count > 0) {
        return halt('请先完成待支付订单');
      }
      const setting = await this.settingService.getDefault();
      if (!!setting.isTradable) {
        return halt('交易暂停');
      }
      return await this.redlock.using([`market:${id}`], 10000, {}, async () => {
        return await this.prisma.$transaction(async (tx) => {
          const asset = await tx.asset.findUnique({
            where: {
              id,
              isDeleted: false,
            },
          });

          if (!asset) {
            return halt('系列信息异常');
          }
          if (!asset.isTradable) {
            return halt('尚未开放寄售');
          }

          let tokens = [];
          if (!!tokenId) {
            const token = await tx.token.findUnique({
              where: {
                id: tokenId,
                assetId: id,
                status: TokenStatusEnum.ON_SALE,
                isDeleted: false,
                ownerId: {
                  not: user.id,
                },
              },
              select: {
                id: true,
                classId: true,
                no: true,
                price: true,
                sellerId: true,
                ownerId: true,
                owner: true,
              },
            });
            tokens = [token];
          } else {
            num = num ?? 1;
            tokens = await tx.token.findMany({
              where: {
                assetId: id,
                status: TokenStatusEnum.ON_SALE,
                isDeleted: false,
                ownerId: {
                  not: user.id,
                },
                price: {
                  lte: price,
                },
              },
              select: {
                id: true,
                classId: true,
                no: true,
                price: true,
                sellerId: true,
                ownerId: true,
                owner: true,
              },
              take: num,
              orderBy: {
                price: 'asc',
              },
            });
          }

          if (tokens.length <= 0) {
            return halt('未找到符合条件的资产');
          }

          let divideDetail = [];
          let token = tokens[0];

          const amount = 0;
          if (token.ownerId !== token.sellerId) {
            return halt('卖家信息异常');
          }
          if (this.config.get('payment') === PaymentTypeEnum.YOP) {
            const res = await this.yopService.memberQuery(
              `${token.owner.id}-${token.owner.code}`,
            );
            if (!res || !res.isSuccess) {
              return halt('卖家钱包信息异常');
            }
            console.log(`用户${token.owner.id}钱包账号: `, res.result);
            divideDetail.push({
              amount: new Decimal(amount).toDecimalPlaces(2).toString(),
              ledgerNo: res.result.memberNo,
              ledgerType: 'MERCHANT2MEMBER',
            });
          }

          const expireTime = dayjs().add(3, 'minutes');

          let no = null;
          let payNo = null;
          let payUrl = null;

          let total = 0;

          for (let t of tokens) {
            total += Number(t.price);
          }

          let fee = total * Number(setting.fee);

          if (this.config.get('isUseBalance')) {
            if (total > Number(user.balance)) {
              return halt('余额不足');
            }
            no = await this.uopService.generateOrderNo();
          } else {
            if (this.config.get('payment') === PaymentTypeEnum.YOP) {
              divideDetail = [
                {
                  amount: new Decimal(fee).toDecimalPlaces(2).toString(),
                  ledgerNo: this.config.get('yop.merCustId'),
                  ledgerType: 'MERCHANT2MERCHANT',
                },
                ...divideDetail,
              ];
              no = await this.yopService.generateOrderNo();
              const res = await this.yopService.order({
                orderId: no,
                orderAmount: new Decimal(total).toDecimalPlaces(2).toNumber(),
                goodsName: `${asset.title} #${token.no}`,
                returnUrl: `${this.yopService.ReturnUrl}/mine`,
                notifyUrl: `${this.yopService.NotifyUrl}/api/order/callback`,
                fundProcessType: 'REAL_TIME_DIVIDE',
                divideDetail,
                businessInfo: {
                  collectionSeries: `${asset.id}`,
                  collectionName: `${asset.title}`,
                  contractModel: '721',
                  collectionId: `${tokenId}`,
                  marketType: '转卖（二级市场）',
                  userRegisterMobile: user.mobile,
                  registTime: dayjs(user.createdAt).format(
                    'YYYY-MM-DD HH:mm:ss',
                  ),
                  registIp: ip,
                  userRegisterIdNo: `${user.idCard}`,
                  registId: `${user.id}`,
                },
              });
              if (!res || !res.isSuccess || res?.result?.code !== '00000') {
                return halt(res?.result?.message ?? '易宝订单异常');
              }
              payNo = res.result.uniqueOrderNo;
              payUrl = res.result.cashierUrl;
            } else if (this.config.get('payment') === PaymentTypeEnum.HUIFU) {
              if (!user.userCustId || !user.acctId) {
                return halt('请先开通汇付钱包');
              }
            } else if (this.config.get('payment') === PaymentTypeEnum.SNAD) {
              no = await this.sandService.generateOrderNo();
              const res = await this.sandService.order({
                orderNo: no,
                orderAmt: new Decimal(total).toDecimalPlaces(2).toString(),
                ip,
                returnUrl: `${this.sandService.ReturnUrl}/mine`,
                notifyUrl: `${this.sandService.NotifyUrl}/api/order/callback`,
                goodsName: `${asset.title} #${token.no}`,
                extra: JSON.stringify({
                  operationType: '1',
                  recvUserId: token.owner.sandId,
                  bizType: '2',
                  payUserId: user.sandId,
                  userFeeAmt: new Decimal(fee).toDecimalPlaces(2).toString(),
                }),
              });
              if (!res || !res.no || !res.url) {
                return halt('杉徳订单异常');
              }
              payNo = res.no;
              payUrl = res.url;
            } else {
              return halt('支付方式异常');
            }
          }

          const order = await tx.order.create({
            data: {
              no,
              num,
              total,
              fee,
              expireTime: expireTime.toDate(),
              assetId: id,
              ownerId: user.id,
              payType: this.config.get('payment'),
              payNo,
              payUrl,
              type: OrderTypeEnum.MARKET,
              status: OrderStatusEnum.PENDING,
            },
          });
          for (let t of tokens) {
            await tx.token.update({
              where: {
                id: t.id,
              },
              data: {
                status: TokenStatusEnum.LOCKING,
              },
            });
            await tx.subOrder.create({
              data: {
                no: await this.uopService.generateOrderNo(),
                price: Number(t.price),
                fee: Number(t.price) * Number(setting.fee),
                orderId: order.id,
                assetId: id,
                tokenId: t.id,
                buyerId: user.id,
                sellerId: t.ownerId,
              },
            });
          }

          await this.orderQueue.add(
            'cancel',
            {
              no: order.no,
            },
            {
              delay: expireTime.diff(dayjs()),
            },
          );

          return { no: order.no };
        });
      });
    });
  }

  async start() {
    const sale = await this.checkQueue.add('check', null, {
      repeat: {
        every: 10 * 1000, // 每10s执行一次
      },
      // removeOnComplete: true,
      // delay: 10000, // 延迟10s
    });
    // console.log(sale.data);
    return true;
  }

  async close() {
    await this.utilService.cleanKeys(`bull:market:repeat`);
    console.log('关闭市场监测');
    return true;
  }
}
